Phoenix LiveView provides powerful JavaScript interoperability features, allowing you to integrate client-side JavaScript code with your server-side LiveView logic.
Setting Up LiveSocket
To enable LiveView client/server interaction, instantiate a LiveSocket:
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
let csrfToken = document . querySelector ( "meta[name='csrf-token']" ). getAttribute ( "content" )
let liveSocket = new LiveSocket ( "/live" , Socket , { params: { _csrf_token: csrfToken }})
liveSocket . connect ()
LiveSocket Options
All options are passed to the Phoenix.Socket constructor, plus these LiveView-specific options:
Option Description bindingPrefixPrefix for Phoenix bindings. Default: "phx-" paramsConnect params passed to view’s mount callback. Can be an object or function hooksUser-defined hooks namespace for client callbacks uploadersUser-defined uploaders for direct-to-cloud uploads metadataAdditional metadata sent with events to the server
Example with all options
let Hooks = {}
Hooks . MyHook = {
mounted () { console . log ( "Mounted!" ) }
}
let liveSocket = new LiveSocket ( "/live" , Socket , {
params: { _csrf_token: csrfToken },
hooks: Hooks ,
metadata: {
click : ( e , el ) => {
return {
altKey: e . altKey ,
clientX: e . clientX ,
clientY: e . clientY
}
}
}
})
LiveSocket Methods
The liveSocket instance exposes these methods:
Connection
Debug
Latency Simulation
JS Execution
JS Commands
Debugging Client Events
Enable debug logging to troubleshoot client-server communication:
// app.js
let liveSocket = new LiveSocket ( ... )
liveSocket . connect ()
window . liveSocket = liveSocket
// In browser console
>> liveSocket . enableDebug ()
Debug state is stored in sessionStorage and persists for the browser session.
Simulating Latency
Test loading states and user experience under network latency:
// In browser console
>> liveSocket . enableLatencySim ( 1000 )
[ Log ] latency simulator enabled for the duration of this browser session .
Call disableLatencySim () to disable
Use latency simulation during development to ensure proper loading state handling, as localhost has near-zero latency.
Handling Server-Pushed Events
Receive events pushed from the server with push_event/3:
Server-side
def handle_info ({ :item_updated , item}, socket) do
{ :noreply , push_event (socket, "highlight" , %{ id: "item- #{ item.id } " })}
end
Client-side
window . addEventListener ( "phx:highlight" , ( e ) => {
let el = document . getElementById ( e . detail . id )
if ( el ) {
// Highlight logic
el . classList . add ( "highlight" )
setTimeout (() => el . classList . remove ( "highlight" ), 1000 )
}
})
Events dispatched from the server are prefixed with phx: in the browser.
Integrating with JS Commands
Embed JS commands in data attributes:
<div id={"item-#{item.id}"} class="item" data-highlight={JS.transition("highlight")}>
{item.title}
</div>
window . addEventListener ( "phx:highlight" , ( e ) => {
document . querySelectorAll ( `[data-highlight]` ). forEach ( el => {
if ( el . id == e . detail . id ){
liveSocket . execJS ( el , el . getAttribute ( "data-highlight" ))
}
})
})
Client Hooks
Hooks provide lifecycle callbacks for custom client-side JavaScript when elements are added, updated, or removed.
Defining a Hook
/**
* @type {import("phoenix_live_view").HooksOptions}
*/
let Hooks = {}
Hooks . PhoneNumber = {
mounted () {
this . el . addEventListener ( "input" , e => {
let match = this . el . value . replace ( / \D / g , "" ). match ( / ^ ( \d {3} )( \d {3} )( \d {4} ) $ / )
if ( match ) {
this . el . value = ` ${ match [ 1 ] } - ${ match [ 2 ] } - ${ match [ 3 ] } `
}
})
}
}
let liveSocket = new LiveSocket ( "/live" , Socket , { hooks: Hooks })
Using the Hook
<input type="text"
name="user[phone_number]"
id="user-phone-number"
phx-hook="PhoneNumber" />
When using phx-hook, a unique DOM ID must always be set.
Hook Lifecycle Callbacks
Callback Description mountedElement added to DOM, LiveView finished mounting beforeUpdateElement about to be updated (must be synchronous) updatedElement updated by the server destroyedElement removed from the page disconnectedParent LiveView disconnected from server reconnectedParent LiveView reconnected to server
Outside a LiveView, only mounted is invoked for elements present at DOM ready.
Hook Attributes and Methods
Hooks have access to:
Attributes
Push Methods
Event Methods
Upload Methods
JS Methods
el - The bound DOM node
liveSocket - The LiveSocket instance
pushEvent(event, payload, callback) - Push event to LiveView server
pushEventTo(target, event, payload, callback) - Push to specific LiveView/Component
handleEvent(event, callback) - Handle server-pushed events
removeHandleEvent(ref) - Remove event handler
upload(name, files) - Inject files into an uploader
uploadTo(target, name, files) - Inject files into specific uploader
js() - Get JS command interface for DOM manipulation
Client-Server Communication
Template
Hook with Reply
Server Handler
<div phx-hook="ClickMeHook" id="click-me">
Click me for a message!
</div>
Receiving Server Events
<div id="chart" phx-hook="Chart"></div>
Scoping Events
For sibling components, namespace events to avoid conflicts:
def update (%{ id: id, points: points} = assigns, socket) do
socket =
socket
|> assign (assigns)
|> push_event ( "points- #{ id } " , points)
{ :ok , socket}
end
Hooks . Chart = {
mounted (){
this . handleEvent ( `points- ${ this . el . id } ` , ( points ) => {
MyChartLib . addPoints ( points )
})
}
}
Colocated Hooks
Define hooks next to component code for better organization:
def phone_number_input (assigns) do
~H"""
< input type = "text"
name = "user[phone_number]"
id = "user-phone-number"
phx-hook = ".PhoneNumber" />
< script :type = {Phoenix.LiveView.ColocatedHook} name = ".PhoneNumber" >
export default {
mounted () {
this . el . addEventListener ( "input" , e => {
let match = this . el . value . replace ( / \D / g , "" ). match ( / ^ ( \d {3} )( \d {3} )( \d {4} ) $ / )
if ( match ) {
this . el . value = ` ${ match [ 1 ] } - ${ match [ 2 ] } - ${ match [ 3 ] } `
}
})
}
}
</ script >
"""
end
Importing Colocated Hooks
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import { hooks as colocatedHooks } from "phoenix-colocated/my_app"
let liveSocket = new LiveSocket ( "/live" , Socket , {
hooks: { ... colocatedHooks }
})
Colocated hooks require the dot syntax (.PhoneNumber) and are automatically prefixed with the module name to prevent conflicts.
Hook as Class
Define hooks as classes extending ViewHook:
import { ViewHook } from "phoenix_live_view"
class MyHook extends ViewHook {
mounted () {
console . log ( "Custom hook mounted" )
}
}
let liveSocket = new LiveSocket ( ... , {
hooks: {
MyHook
}
})
Advanced DOM Integration
For libraries requiring full DOM control, use onBeforeElUpdated:
let liveSocket = new LiveSocket ( "/live" , Socket , {
hooks: Hooks ,
dom: {
onBeforeElUpdated ( from , to ) {
// Preserve client-side attributes
for ( const attr of from . attributes ) {
if ( attr . name . startsWith ( "data-js-" )) {
to . setAttribute ( attr . name , attr . value )
}
}
}
}
})
The onBeforeElUpdated callback is called just before DOM patching. The operation cannot be cancelled or deferred.
JS Commands from JavaScript
Execute JS commands from hooks or client code:
// In a hook
this . js (). show ( this . el , { transition: "fade-in" })
// Via LiveSocket
const el = document . querySelector ( "#my-element" )
liveSocket . js (). hide ( el , { transition: "fade-out" })
Available JS Methods
Visibility
Classes
Attributes
Transitions
Navigation
show(el, opts) - Show element
hide(el, opts) - Hide element
toggle(el, opts) - Toggle visibility
addClass(el, names, opts) - Add CSS classes
removeClass(el, names, opts) - Remove CSS classes
toggleClass(el, names, opts) - Toggle CSS classes
setAttribute(el, attr, val) - Set attribute
removeAttribute(el, attr) - Remove attribute
toggleAttribute(el, attr, val1, val2) - Toggle attribute
transition(el, transition, opts) - Apply CSS transition
push(el, event, opts) - Push event to server
navigate(href, opts) - Navigate with pushState
patch(href, opts) - Patch with pushState
See JS Commands for complete documentation.
Combine hooks with data attributes for complex interactions:
<div id="infinite-scroll"
phx-hook="InfiniteScroll"
data-page={@page}>
<!-- Content -->
</div>
Hooks . InfiniteScroll = {
page () { return this . el . dataset . page },
mounted (){
this . pending = this . page ()
window . addEventListener ( "scroll" , e => {
if ( this . pending == this . page () && scrollAt () > 90 ){
this . pending = this . page () + 1
this . pushEvent ( "load-more" , {})
}
})
},
updated (){ this . pending = this . page () }
}
See Also